package org.infodavid.common.persistence;

import java.sql.Connection;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;

import javax.transaction.UserTransaction;

import junit.framework.Assert;

import org.hsqldb.util.DatabaseManager;
import org.infodavid.common.persistence.exceptions.DataLockedException;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.objectweb.jotm.Jotm;

import com.mockrunner.ejb.EJBTestModule;
import com.mockrunner.mock.ejb.EJBMockObjectFactory;

/**
 * The Class AbstractPersistenceSessionTest.
 */
public abstract class AbstractPersistenceSessionTest {

  /** The Constant ITEMS_TO_INSERT. */
  protected static final int ITEMS_TO_INSERT = 10;
  /** The random utility. */
  private static final Random RAND = new Random();
  /** The session. */
  private static PersistenceSession session;
  /** JTA implementation. */
  private static Jotm jotm = null;
  /** User transaction. */
  private static UserTransaction userTransaction = null;
  /** EJB Mock Object Factory. */
  private static EJBMockObjectFactory factory = null;
  /** Used to set up JNDI for test. */
  protected static EJBTestModule ejbModule = null;

  /**
   * Sets the up.
   * @throws Exception the exception
   */
  @BeforeClass
  public static void setUpClass() throws Exception {
    jotm = new Jotm(true, false);
    userTransaction = jotm.getUserTransaction();
    factory = new EJBMockObjectFactory();
    ejbModule = new EJBTestModule(factory);
    ejbModule.bindToContext(ITransaction.USER_TRANSACTION_JNDI, userTransaction);
    ejbModule.bindToContext("java:/" + ITransaction.TRANSACTION_MANAGER_JNDI,
        jotm.getTransactionManager());

    runDatabaseManager();
  }

  /**
   * Run database manager.
   */
  private static void runDatabaseManager() {
    final String url = "jdbc:hsqldb:mem:MyDataBase";

    try {
      Class.forName("org.hsqldb.jdbcDriver");

      final Connection con = DriverManager.getConnection(url, "sa", "");

      if (con != null) {
        // needed to patch DatabaseManager so it could
        // be initialized and use the supplied connection
        final DatabaseManager dm = new DatabaseManager();

        dm.main();
        dm.connect(con);
      }
    }
    catch (final Exception e) {
      // System.out.println("Can't open the database manager: " + e.getMessage());
    }
  }

  /**
   * Sets the up.
   * @throws Exception the exception
   */
  @Before
  public void setUp() throws Exception {
    if (session == null || !session.isOpen()) {
      session = buildPersistenceSession();
    }

    session.getTransaction().begin();
    session.removeEntities(session.findEntities(DataStub.class, DataStub.QRY_FIND_ALL, null), null);
    session.getTransaction().commit();

    final long result = getCount();

    Assert.assertEquals("Not all insertedObjects has been removed", 0, result);

    session.getTransaction().begin();
  }

  /**
   * Tear down.
   * @throws Exception the exception
   */
  @After
  public void tearDown() throws Exception {
    if (session != null && session.isOpen()) {
      try {
        if (session.isTransactionActive()) {
          session.getTransaction().commit();
        }

        session.close();
      }
      catch (final Exception e) {
        e.printStackTrace();
      }
    }
  }

  protected abstract PersistenceSession buildPersistenceSession() throws Exception;

  /**
   * Tear down.
   * @throws Exception the exception
   */
  @AfterClass
  public static void tearDownClass() throws Exception {
    if (session.isOpen()) {
      try {
        if (session.isTransactionActive()) {
          session.getTransaction().commit();
        }
      }
      catch (final Exception e) {
        // e.printStackTrace();
      }

      // System.out.println(new MetadataHelper(session.getConnection(), "%"));

      try {
        session.close();
      }
      catch (final Exception e) {
        // e.printStackTrace();
      }
    }
  }

  /**
   * Returns the number of rows.
   * @return the number of rows
   * @throws Exception
   */
  public long getCount() throws Exception {
    final Object obj = session.executeQuery(DataStub.class, "selectCount", null);

    return obj instanceof Number ? ((Number)obj).longValue() : 0;
  }

  /**
   * Test close.
   * @throws Exception the exception
   */
  @Test
  public final void testClose() throws Exception {
    if (session.isOpen()) {
      session.close();
    }

    Assert.assertFalse("Connection is active", session.isOpen());
  }

  /**
   * Test execute.
   * @throws Exception the exception
   */
  @Test
  public final void testExecuteT() throws Exception {
    populate(ITEMS_TO_INSERT);

    final long result = getCount();

    Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT, result);
  }

  /**
   * Test find entities.
   * @throws Exception the exception
   */
  @Test
  public final void testFindEntities() throws Exception {
    populate(ITEMS_TO_INSERT);

    final Collection<DataStub> result =
        session.findEntities(DataStub.class, DataStub.QRY_FIND_ALL, null);

    // System.out.println(result);

    Assert.assertNotNull("Result is null", result);
    Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT, result.size());
  }

  /**
   * Test find entities.
   * @throws Exception the exception
   */
  @Test
  public final void testFindEntitiesMaxRows() throws Exception {
    final List<IQueryParameter> params = new ArrayList<IQueryParameter>();
    final IQueryParameter param1 = QueryParameterFactory.getSingleton().borrow();

    try {
      populate(ITEMS_TO_INSERT);

      param1.setType(IQueryParameter.EParameterType.MAX_ROWS).setValue(Integer.valueOf(5));
      params.add(param1);

      final Collection<DataStub> result =
          session.findEntities(DataStub.class, DataStub.QRY_FIND_ALL, params);

      // System.out.println(result);

      Assert.assertNotNull("Result is null", result);
      Assert.assertEquals("Not all data has been inserted", 5, result.size());
    }
    finally {
      QueryParameterFactory.getSingleton().release(param1);
    }
  }

  /**
   * Test find entities.
   * @throws Exception the exception
   */
  @Test
  public final void testFindEntitiesFirstRows() throws Exception {
    final List<IQueryParameter> params = new ArrayList<IQueryParameter>();
    final IQueryParameter param1 = QueryParameterFactory.getSingleton().borrow();

    try {
      populate(ITEMS_TO_INSERT);

      param1.setType(IQueryParameter.EParameterType.FIRST_ROW).setValue(Integer.valueOf(5));
      params.add(param1);

      final Collection<DataStub> result =
          session.findEntities(DataStub.class, DataStub.QRY_FIND_ALL, params);

      // System.out.println(result);

      Assert.assertNotNull("Result is null", result);
      Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT - 5, result.size());
    }
    finally {
      QueryParameterFactory.getSingleton().release(param1);
    }
  }

  /**
   * Test get delegate.
   * @throws Exception the exception
   */
  @Test
  public final void testGetDelegate() throws Exception {
    Assert.assertNotNull("Delegate is null", session.getDelegate());
  }

  /**
   * Test get transaction.
   * @throws Exception the exception
   */
  @Test
  public final void testGetTransaction() throws Exception {
    Assert.assertNotNull("Transaction object is null", session.getTransaction());
  }

  /**
   * Populate.
   * @param count the count
   * @return the collection< test data>
   * @throws Exception the exception
   */
  private Collection<DataStub> populate(final int count) throws Exception {
    Collection<DataStub> inserted = null;
    DataStub item;

    if (count > 1) {
      final List<DataStub> items = new ArrayList<DataStub>(count);

      for (int i = 0; i < count; i++) {
        item = new DataStub();
        item.setArchived(RAND.nextBoolean());
        items.add(item);
      }

      inserted = session.insertEntities(items, null);
    }
    else {
      item = new DataStub();
      item.setArchived(RAND.nextBoolean());
      final DataStub result = session.insertEntity(item);

      if (result != null) {
        inserted = Collections.singletonList(result);
      }
    }

    Assert.assertEquals("Not all the items were inserted", count, getCount());

    return inserted;
  }

  /**
   * Test insert entities.
   * @throws Exception the exception
   */
  @Test
  public final void testInsertEntities() throws Exception {
    final Collection<DataStub> inserted = populate(ITEMS_TO_INSERT);

    Assert.assertNotNull("Result is null", inserted);
    Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT, inserted.size());

    final long result = getCount();

    // System.out.println(result);

    Assert.assertEquals("Not all data has been inserted", ITEMS_TO_INSERT, result);
  }

  /**
   * Test insert entity.
   * @throws Exception the exception
   */
  @Test
  public final void testInsertEntity() throws Exception {
    final Collection<DataStub> inserted = populate(1);

    Assert.assertNotNull("Result is null", inserted);
    Assert.assertEquals("Not all data has been inserted", 1, inserted.size());

    final long result = getCount();

    // System.out.println(result);

    Assert.assertEquals("Not all data has been inserted", 1, result);
  }

  /**
   * Test insert entity.
   * @throws Exception the exception
   */
  @Test
  public final void testRollback() throws Exception {
    final Collection<DataStub> inserted = populate(10);

    Assert.assertNotNull("Result is null", inserted);
    Assert.assertEquals("Not all data has been inserted", 10, inserted.size());

    session.getTransaction().rollback();

    final long result = getCount();

    // System.out.println(result);

    Assert.assertEquals("Not all data has been inserted", 0, result);
  }

  /**
   * Test is open.
   * @throws Exception the exception
   */
  @Test
  public final void testIsOpen() throws Exception {
    Assert.assertTrue("Connection is closed", session.isOpen());

    testClose();
  }

  /**
   * Test lock.
   * @throws Exception the exception
   */
  @Test
  public final void testLock() throws Exception {
    final List<IQueryParameter> params = new ArrayList<IQueryParameter>();
    final IQueryParameter param1 = QueryParameterFactory.getSingleton().borrow();
    final IQueryParameter param2 = QueryParameterFactory.getSingleton().borrow();
    Thread thread = null;

    try {
      final Collection<DataStub> inserted = populate(ITEMS_TO_INSERT);

      param1.setName(IDataObject.ARCHIVED).setValue(Boolean.FALSE);
      params.add(param1);
      param2.setType(IQueryParameter.EParameterType.MAX_ROWS).setValue(Integer.valueOf(1));
      params.add(param2);

      session.findEntities(DataStub.class, "findArchived", params);

      // System.out.println(result);

      final DataStub data = inserted.iterator().next();

      Assert.assertNotNull("Result is null", data);

      thread =
          new LockingClient().newLockingThread(data.getClass(), data.getKey(),
              ILockable.ELockType.NO_WAIT);
      thread.start();
      session.lock(data, ILockable.ELockType.NO_WAIT);
    }
    catch (final DataLockedException e) {
      e.printStackTrace();
    }
    catch (final Exception e) {
      Assert.fail(e.getMessage());
    }
    finally {
      if (thread != null) {
        try {
          thread.interrupt();
        }
        catch (final Exception e) {}
      }

      QueryParameterFactory.getSingleton().release(param1);
      QueryParameterFactory.getSingleton().release(param2);

      if (session.isOpen()) {
        session.close();
      }
    }
  }

  /**
   * Test remove entities.
   * @throws Exception the exception
   */
  @Test
  public final void testRemoveEntities() throws Exception {
    final Collection<DataStub> inserted = populate(ITEMS_TO_INSERT);

    session.removeEntities(inserted, null);
    session.getTransaction().commit();
    session.getTransaction().begin();

    Assert.assertEquals("Not all data has been inserted", 0, getCount());
  }

  /**
   * Test remove entity.
   * @throws Exception the exception
   */
  @Test
  public final void testRemoveEntity() throws Exception {
    final Collection<DataStub> inserted = populate(ITEMS_TO_INSERT);

    session.removeEntity(inserted.iterator().next());

    Thread.sleep(100);

    Assert.assertEquals("Not all data has been removed", ITEMS_TO_INSERT - 1, getCount());
  }

  /**
   * Test update entities.
   * @throws Exception the exception
   */
  @Test
  public final void testUpdateEntities() throws Exception {
    final List<IQueryParameter> params = new ArrayList<IQueryParameter>();
    final IQueryParameter param = QueryParameterFactory.getSingleton().borrow();

    try {
      final Collection<DataStub> inserted = populate(ITEMS_TO_INSERT);

      for (final DataStub data : inserted) {
        data.setArchived(true);
      }

      session.updateEntities(inserted, null);
      session.getTransaction().commit();

      param.setName(IDataObject.ARCHIVED).setValue(Boolean.FALSE);
      params.add(param);

      final Collection<DataStub> result =
          session.findEntities(DataStub.class, "findArchived", params);

      // System.out.println(result);

      Assert.assertNotNull("Result is null", result);
      Assert.assertEquals("Not all data has been inserted", 0, result.size());
    }
    finally {
      QueryParameterFactory.getSingleton().release(param);
    }
  }

  /**
   * Test update entity.
   * @throws Exception the exception
   */
  @Test
  public final void testUpdateEntity() throws Exception {
    final List<IQueryParameter> params = new ArrayList<IQueryParameter>();
    final IQueryParameter param = QueryParameterFactory.getSingleton().borrow();

    try {
      final Collection<DataStub> inserted = populate(ITEMS_TO_INSERT);

      for (final DataStub data : inserted) {
        if (!data.isArchived()) {
          data.setArchived(true);
          session.updateEntity(data);
        }
      }

      session.getTransaction().commit();

      param.setName(IDataObject.ARCHIVED).setValue(Boolean.FALSE);
      params.add(param);

      final Collection<DataStub> result =
          session.findEntities(DataStub.class, DataStub.QRY_FIND_ARCHIVED, params);

      // System.out.println(result);

      Assert.assertNotNull("Result is null", result);
      Assert.assertEquals("Not all data has been inserted", 0, result.size());
    }
    finally {
      QueryParameterFactory.getSingleton().release(param);
    }
  }
}
